package de.hauschild.gmltracer.tracer.impl.tier1;

import java.util.List;

import org.apache.commons.math.geometry.Vector3D;

import de.hauschild.gmltracer.tracer.Intersection;
import de.hauschild.gmltracer.tracer.Ray;
import de.hauschild.gmltracer.tracer.Shape;
import de.hauschild.gmltracer.tracer.SurfaceFunction;
import de.hauschild.gmltracer.tracer.impl.AbstractShape;

/**
 * An abstract extension of {@link AbstractShape} providing basic functionality for a bounding sphere. Around the
 * concrete shape will be a sphere with a radius equals to the distance of the fares point of the concrete shape. The
 * {@link #intersectAfterIgnoration(Ray, List)} will first test intersection with the bounding sphere. If the sphere was
 * intersected the concrete shape has to perform a detailed intersection test. If the sphere was not hit, the concrete
 * shape wasn't hit too.
 * 
 * @since 1.0.0
 * @author Klaus Hauschild
 */
public abstract class AbstractBoundingSpheredShape extends AbstractShape {

  private final Sphere boundingSphere;

  /**
   * Instantiates a new {@link AbstractBoundingSpheredShape}.
   * 
   * @param theSurfaceFunction
   *          the the surface function
   * @param boundingSphereRadius
   *          the bounding sphere radius
   */
  public AbstractBoundingSpheredShape(final SurfaceFunction theSurfaceFunction, final double boundingSphereRadius) {
    super(theSurfaceFunction);
    // set up bounding sphere
    boundingSphere = createBoundingSphere(boundingSphereRadius);
  }

  @Override
  public void rotateX(final double degrees) {
    super.rotateX(degrees);
    if (boundingSphere != null) {
      boundingSphere.rotateX(degrees);
    }
  }

  @Override
  public void rotateY(final double degrees) {
    super.rotateY(degrees);
    if (boundingSphere != null) {
      boundingSphere.rotateY(degrees);
    }
  }

  @Override
  public void rotateZ(final double degrees) {
    super.rotateZ(degrees);
    if (boundingSphere != null) {
      boundingSphere.rotateZ(degrees);
    }
  }

  @Override
  public void scale(final double x, final double y, final double z) {
    super.scale(x, y, z);
    if (boundingSphere != null) {
      boundingSphere.scale(x, y, z);
    }
  }

  @Override
  public void translate(final double x, final double y, final double z) {
    super.translate(x, y, z);
    if (boundingSphere != null) {
      boundingSphere.translate(x, y, z);
    }
  }

  @Override
  public void uniformScale(final double scale) {
    super.uniformScale(scale);
    if (boundingSphere != null) {
      boundingSphere.uniformScale(scale);
    }
  }

  /**
   * Intersect after bounding sphere was intersected.
   * 
   * @param ray
   *          the ray
   * @param boundingSphereIntersection
   *          the bounding sphere intersection
   * @return the intersection
   */
  protected abstract Intersection intersectAfterBoundingSphere(Ray ray, Intersection boundingSphereIntersection);

  @Override
  protected Intersection intersectAfterIgnoration(final Ray ray, final List<Shape> shapesToIngore) {
    // bounding sphere intersection test
    final Intersection boundingSphereIntersection = sphereIntersection(ray);
    // bounding sphere was intersected
    if (boundingSphereIntersection != null) {
      return intersectAfterBoundingSphere(ray, boundingSphereIntersection);
    }
    // no intersection
    return null;
  }

  /**
   * Creates a bounding sphere if needed. Is the concrete shape a sphere too, the bounding sphere isn't needed.
   * 
   * @param boundingSphereRadius
   *          the bounding sphere radius
   * @return the created bounding sphere
   */
  private Sphere createBoundingSphere(final double boundingSphereRadius) {
    if (!(this instanceof Sphere)) {
      final Sphere sphere = new Sphere(new NopSurfaceFunction());
      sphere.uniformScale(boundingSphereRadius);
      return sphere;
    }
    return null;
  }

  /**
   * Sphere intersection.
   * 
   * @param ray
   *          the ray
   * @return the intersection
   */
  private Intersection sphereIntersection(final Ray ray) {
    final Vector3D position = worldToObject(ray.getStart());
    final Vector3D direction = worldToObject(ray.getEnd()).subtract(position).normalize();

    final double a = 1;
    final double b = 2.0 * Vector3D.dotProduct(direction, position);
    final double c = Vector3D.dotProduct(position, position) - 1;
    final double d = b * b - 4 * a * c;

    if (d < 0.0) {
      return null;
    }

    final double sqrtD = Math.sqrt(d);
    double q;
    if (b < 0) {
      q = (-b - sqrtD) / 2.0;
    } else {
      q = (-b + sqrtD) / 2.0;
    }

    double t0 = q / a;
    double t1 = c / q;

    if (t0 > t1) {
      final double temp = t0;
      t0 = t1;
      t1 = temp;
    }

    if (t1 < 0) {
      return null;
    }

    double t;
    if (t0 < 0) {
      t = t1;
    } else {
      t = t0;
    }

    final Vector3D hitPoint = direction.scalarMultiply(t).add(position);
    return new Intersection(objectToWorld(hitPoint), this, hitPoint);
  }
}
